@memorysmithapp/markdown-renderer 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/Markdown.d.ts +12 -1
- package/dist/Markdown.js +41 -33
- package/dist/diagnose.js +15 -1
- package/dist/edit/ContextMenu.d.ts +1 -1
- package/dist/edit/ContextMenu.js +1 -1
- package/dist/edit/NoteEditor.js +42 -21
- package/dist/edit/blocks.d.ts +21 -5
- package/dist/edit/blocks.js +328 -56
- package/dist/edit/click-source.d.ts +16 -0
- package/dist/edit/click-source.js +74 -0
- package/dist/edit/heights.js +2 -0
- package/dist/edit/image-size.d.ts +26 -10
- package/dist/edit/image-size.js +52 -17
- package/dist/edit/live.js +25 -11
- package/dist/host.d.ts +15 -1
- package/dist/image-dimensions.d.ts +30 -5
- package/dist/image-dimensions.js +39 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/kept-image.d.ts +20 -0
- package/dist/kept-image.js +47 -0
- package/dist/markdown.js +2 -4
- package/dist/mode-icons.d.ts +3 -0
- package/dist/mode-icons.js +7 -2
- package/dist/strings.d.ts +18 -1
- package/dist/strings.js +10 -1
- package/dist/theme.d.ts +8 -1
- package/package.json +2 -2
- package/structure.css +46 -8
- package/theme-default.css +48 -7
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ import 'katex/dist/katex.min.css';
|
|
|
18
18
|
<NoteBody
|
|
19
19
|
markdown={note} // the note as you keep it, frontmatter included
|
|
20
20
|
resolve={(name) => addressOfTheOneNoteNamed(name)} // or null: the link is pending
|
|
21
|
-
keeps={(name) => notebookKeepsAFile(name)} //
|
|
21
|
+
keeps={(name) => notebookKeepsAFile(name)} // reached by an embed, and by an image with no host: 
|
|
22
22
|
fileOf={(name) => describe(name)} // { mimeType, bytes, url, description }: drawn by its type, as MemorySmith does
|
|
23
23
|
renderLink={({ href, className, children }) => <a href={href} className={className} onClick={follow(href)}>{children}</a>} // follow: your router's navigate, captured where it is
|
|
24
24
|
loadNote={(name) => fetchTheBodyOf(name)} // for an embedded note; the package never fetches
|
package/dist/Markdown.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnchorHTMLAttributes, type ReactNode } from 'react';
|
|
1
|
+
import { type AnchorHTMLAttributes, type ImgHTMLAttributes, type ReactNode } from 'react';
|
|
2
2
|
import { type FootnoteRef } from './footnotes.js';
|
|
3
3
|
import type { TaskSave } from './task-saves.js';
|
|
4
4
|
/**
|
|
@@ -40,6 +40,17 @@ export interface MarkdownProps {
|
|
|
40
40
|
type WithNode<T> = T & {
|
|
41
41
|
node?: unknown;
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* An image, with the dimensions the specification puts in its alt text
|
|
45
|
+
* (§3.14). What precedes the pipe is the description and is never dropped;
|
|
46
|
+
* what follows it is width, or width and height, in CSS pixels, or a width as
|
|
47
|
+
* a share of the page at most (#108), and never appears as text. A value that is neither stays part of the description,
|
|
48
|
+
* because deleting an accessibility label is the worse failure.
|
|
49
|
+
*
|
|
50
|
+
* A destination that names no host is a file the notebook keeps, by the last segment of its path
|
|
51
|
+
* (§5.8, #104): it is drawn as the embed of that file is, and never asked of a path under the page.
|
|
52
|
+
*/
|
|
53
|
+
export declare function MarkdownImage({ alt, node: _node, ...rest }: WithNode<ImgHTMLAttributes<HTMLImageElement>>): import("react").JSX.Element;
|
|
43
54
|
export declare function MarkdownAnchor({ href, children, node: _node, ...rest }: WithNode<AnchorHTMLAttributes<HTMLAnchorElement>>): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
|
|
44
55
|
export declare function Markdown({ children, source, onToggleTask, taskSave, writable, footnotes }: MarkdownProps): import("react").JSX.Element;
|
|
45
56
|
export {};
|
package/dist/Markdown.js
CHANGED
|
@@ -7,9 +7,10 @@ import rehypeKatex from 'rehype-katex';
|
|
|
7
7
|
import { followable } from './address.js';
|
|
8
8
|
import { CopyButton } from './CopyButton.js';
|
|
9
9
|
import { fileAddress, fileKind, formatBytes, opensInBrowser, rendersAs } from './file-types.js';
|
|
10
|
+
import { keptImageName } from './kept-image.js';
|
|
10
11
|
import { highlightLanguage, highlightTree, languageToLoad, loadGrammar } from './highlight.js';
|
|
11
12
|
import { useHost } from './host.js';
|
|
12
|
-
import { readDimensions, readImageAlt } from './image-dimensions.js';
|
|
13
|
+
import { readDimensions, readImageAlt, sizedAttributes } from './image-dimensions.js';
|
|
13
14
|
import { choiceTargetOf, LinkChoice } from './LinkChoice.js';
|
|
14
15
|
import { toUnixNewlines } from './markdown.js';
|
|
15
16
|
import { MermaidDiagram } from './MermaidDiagram.js';
|
|
@@ -20,13 +21,21 @@ import { ordinalAt } from './tasklist.js';
|
|
|
20
21
|
/**
|
|
21
22
|
* An image, with the dimensions the specification puts in its alt text
|
|
22
23
|
* (§3.14). What precedes the pipe is the description and is never dropped;
|
|
23
|
-
* what follows it is width, or width and height, in CSS pixels,
|
|
24
|
-
* appears as text. A value that is neither stays part of the description,
|
|
24
|
+
* what follows it is width, or width and height, in CSS pixels, or a width as
|
|
25
|
+
* a share of the page at most (#108), and never appears as text. A value that is neither stays part of the description,
|
|
25
26
|
* because deleting an accessibility label is the worse failure.
|
|
27
|
+
*
|
|
28
|
+
* A destination that names no host is a file the notebook keeps, by the last segment of its path
|
|
29
|
+
* (§5.8, #104): it is drawn as the embed of that file is, and never asked of a path under the page.
|
|
26
30
|
*/
|
|
27
|
-
function MarkdownImage({ alt, node: _node, ...rest }) {
|
|
31
|
+
export function MarkdownImage({ alt, node: _node, ...rest }) {
|
|
28
32
|
const read = readImageAlt(alt ?? '');
|
|
29
|
-
|
|
33
|
+
const name = typeof rest.src === 'string' ? keptImageName(rest.src) : null;
|
|
34
|
+
if (name !== null)
|
|
35
|
+
return _jsx(Attachment, { name: name, size: read, description: read.description, title: rest.title });
|
|
36
|
+
// The description is the hover hint too, so a person who sees the picture can read it (#109); a title written wins.
|
|
37
|
+
const title = rest.title || read.description || undefined;
|
|
38
|
+
return _jsx("img", { ...rest, alt: read.description, title: title, ...sizedAttributes(read) });
|
|
30
39
|
}
|
|
31
40
|
/**
|
|
32
41
|
* A link to a place in the same note: a footnote and its way back (§7.12),
|
|
@@ -56,26 +65,19 @@ function InPageAnchor({ href, children, ...rest }) {
|
|
|
56
65
|
};
|
|
57
66
|
return (_jsx("a", { href: href, onClick: follow, ...rest, children: children }));
|
|
58
67
|
}
|
|
59
|
-
|
|
60
|
-
* A file the notebook keeps, where a note embedded it, drawn by its type — an
|
|
61
|
-
* image, audio, video, or a card — by the host's `renderAttachment`, else from
|
|
62
|
-
* what its `fileOf` says (#77). A name the notebook keeps nothing under is
|
|
63
|
-
* reported the way a pending link is: a notebook is read most while it is
|
|
64
|
-
* being written (§5.8).
|
|
65
|
-
*/
|
|
66
|
-
function Attachment({ name, width, height }) {
|
|
68
|
+
function Attachment({ name, size, ...written }) {
|
|
67
69
|
const host = useHost();
|
|
68
70
|
if (!host.keeps(name))
|
|
69
|
-
return _jsx(AttachmentMissing, { name: name });
|
|
71
|
+
return _jsx(AttachmentMissing, { name: name, label: written.description });
|
|
70
72
|
if (host.renderAttachment)
|
|
71
|
-
return _jsx(_Fragment, { children: host.renderAttachment({ name,
|
|
73
|
+
return _jsx(_Fragment, { children: host.renderAttachment({ name, ...size, ...written }) });
|
|
72
74
|
if (!host.fileOf)
|
|
73
|
-
return _jsx("span", { className: "attachment-loading", children: name });
|
|
74
|
-
return _jsx(KeptAttachment, { name: name,
|
|
75
|
+
return _jsx("span", { className: "attachment-loading", children: written.description || name });
|
|
76
|
+
return _jsx(KeptAttachment, { name: name, size: size, ...written });
|
|
75
77
|
}
|
|
76
|
-
function AttachmentMissing({ name }) {
|
|
78
|
+
function AttachmentMissing({ name, label }) {
|
|
77
79
|
const host = useHost();
|
|
78
|
-
return (_jsx("span", { className: "attachment-missing", title: host.strings.attachmentMissing, children: name }));
|
|
80
|
+
return (_jsx("span", { className: "attachment-missing", title: host.strings.attachmentMissing, "data-name": label ? name : undefined, children: label || name }));
|
|
79
81
|
}
|
|
80
82
|
function isThenable(value) {
|
|
81
83
|
return typeof value === 'object' && value !== null && typeof value.then === 'function';
|
|
@@ -129,30 +131,36 @@ function openInTab(event, url) {
|
|
|
129
131
|
* included — a card with what it is, its size, and the ways out of the page: Open only for what a
|
|
130
132
|
* browser shows, Download always. A theme may draw it through the `attachment` slot.
|
|
131
133
|
*/
|
|
132
|
-
function KeptAttachment({ name,
|
|
134
|
+
function KeptAttachment({ name, size: declared, ...written }) {
|
|
133
135
|
const host = useHost();
|
|
134
136
|
const slot = useSlot('attachment');
|
|
135
137
|
const described = useKeptFile(name);
|
|
136
138
|
if (described === undefined)
|
|
137
|
-
return _jsx("span", { className: "attachment-loading", children: name });
|
|
139
|
+
return _jsx("span", { className: "attachment-loading", children: written.description || name });
|
|
138
140
|
const url = described ? fileAddress(described.url) : '';
|
|
139
141
|
if (!described || url === '')
|
|
140
|
-
return _jsx(AttachmentMissing, { name: name });
|
|
141
|
-
|
|
142
|
+
return _jsx(AttachmentMissing, { name: name, label: written.description });
|
|
143
|
+
// What the note wrote of the file is what the reader gets of it; the file's own when it wrote nothing.
|
|
144
|
+
const description = written.description || described.description;
|
|
145
|
+
const file = {
|
|
146
|
+
...described,
|
|
147
|
+
url,
|
|
148
|
+
downloadUrl: fileAddress(described.downloadUrl ?? described.url) || url,
|
|
149
|
+
...(description ? { description } : {}),
|
|
150
|
+
};
|
|
142
151
|
const shape = rendersAs(file.mimeType);
|
|
143
152
|
const kind = fileKind(file.mimeType, host.strings.fileKinds);
|
|
144
153
|
const size = formatBytes(file.bytes, host.strings.locale);
|
|
145
154
|
const opens = opensInBrowser(file.mimeType);
|
|
146
155
|
if (slot)
|
|
147
|
-
return _jsx(_Fragment, { children: drawSlot(slot, { name, file, shape, kind, size, opens,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return _jsx("img", { className: "attachment-image", src: file.url, alt: file.description || name, ...sized });
|
|
156
|
+
return _jsx(_Fragment, { children: drawSlot(slot, { name, file, shape, kind, size, opens, ...declared, title: written.title ?? null }) });
|
|
157
|
+
const sized = sizedAttributes(declared);
|
|
158
|
+
if (shape === 'image') {
|
|
159
|
+
// Described, its description is also the hover hint, a title written winning (#109); a picture
|
|
160
|
+
// with none takes its name as its `alt` and no hint, which would only say the name again.
|
|
161
|
+
const title = written.title || file.description;
|
|
162
|
+
return _jsx("img", { className: "attachment-image", src: file.url, alt: file.description || name, ...(title ? { title } : {}), ...sized });
|
|
163
|
+
}
|
|
156
164
|
if (shape === 'audio')
|
|
157
165
|
return _jsx("audio", { className: "attachment-player", controls: true, src: file.url });
|
|
158
166
|
if (shape === 'video')
|
|
@@ -178,7 +186,7 @@ export function MarkdownAnchor({ href, children, node: _node, ...rest }) {
|
|
|
178
186
|
const at = address.lastIndexOf('|');
|
|
179
187
|
const measured = at === -1 ? null : readDimensions(address.slice(at + 1));
|
|
180
188
|
const name = measured ? address.slice(0, at) : address;
|
|
181
|
-
return _jsx(Attachment, { name: name,
|
|
189
|
+
return _jsx(Attachment, { name: name, size: measured ?? { width: null, height: null, percent: null } });
|
|
182
190
|
}
|
|
183
191
|
if (href?.startsWith('pending:')) {
|
|
184
192
|
const pending = (label) => (_jsx("span", { className: "wikilink-pending", title: host.strings.pendingLink, children: label }));
|
package/dist/diagnose.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* note, or beside an editor. KaTeX checks a formula as `rehype-katex` would typeset it, and Mermaid
|
|
12
12
|
* reads a diagram as the page would draw it, loaded only when a note carries one.
|
|
13
13
|
*/
|
|
14
|
+
import { keptImageName } from './kept-image.js';
|
|
14
15
|
import katex from 'katex';
|
|
15
16
|
import remarkParse from 'remark-parse';
|
|
16
17
|
import { unified } from 'unified';
|
|
@@ -36,7 +37,7 @@ export const NOTICES = {
|
|
|
36
37
|
'raw-html': { severity: 'notice', section: '7.9', message: 'Raw HTML is drawn as text, tags and all.' },
|
|
37
38
|
'strikethrough-single-tilde': { severity: 'notice', section: '4.3', message: 'One tilde strikes nothing: the tildes stay on the page. Strikethrough is ~~two~~.' },
|
|
38
39
|
'link-pending': { severity: 'hint', section: '7.4', message: 'No note answers this link yet, so it is drawn as pending.' },
|
|
39
|
-
'embed-pending': { severity: 'hint', section: '7.3', message: 'No note and no kept file answers this embed, so it is drawn as
|
|
40
|
+
'embed-pending': { severity: 'hint', section: '7.3', message: 'No note and no kept file answers this embed, or no kept file this image, so it is drawn as pending.' },
|
|
40
41
|
};
|
|
41
42
|
/** A footnote reference left as text, which is one with no definition (§7.12). */
|
|
42
43
|
const UNDEFINED_REFERENCE = /\[\^[^\]\s]+\]/g;
|
|
@@ -106,6 +107,7 @@ export async function diagnose(markdown, options = {}) {
|
|
|
106
107
|
const diagrams = [];
|
|
107
108
|
const referenced = new Set();
|
|
108
109
|
const definitions = [];
|
|
110
|
+
const images = [];
|
|
109
111
|
walk(tree, (node) => {
|
|
110
112
|
const at = span(node);
|
|
111
113
|
if (!at)
|
|
@@ -136,6 +138,11 @@ export async function diagnose(markdown, options = {}) {
|
|
|
136
138
|
else if (node.type === 'footnoteDefinition') {
|
|
137
139
|
definitions.push(node);
|
|
138
140
|
}
|
|
141
|
+
else if (node.type === 'image') {
|
|
142
|
+
const name = keptImageName(node.url ?? '');
|
|
143
|
+
if (name !== null)
|
|
144
|
+
images.push({ name, at });
|
|
145
|
+
}
|
|
139
146
|
});
|
|
140
147
|
for (const definition of definitions) {
|
|
141
148
|
const at = span(definition);
|
|
@@ -164,6 +171,13 @@ export async function diagnose(markdown, options = {}) {
|
|
|
164
171
|
}
|
|
165
172
|
});
|
|
166
173
|
pending(hidden, split, options, report);
|
|
174
|
+
// An image whose destination names no host names a kept file, and only a kept file (§5.8, #104).
|
|
175
|
+
if (options.resolve || options.reaches) {
|
|
176
|
+
const keeps = options.keeps ?? (() => false);
|
|
177
|
+
for (const image of images)
|
|
178
|
+
if (!keeps(image.name))
|
|
179
|
+
report('embed-pending', image.at[0], image.at[1], image.name);
|
|
180
|
+
}
|
|
167
181
|
if (diagrams.length > 0 && options.diagrams !== false) {
|
|
168
182
|
const mermaid = (await import('mermaid')).default;
|
|
169
183
|
for (const diagram of diagrams) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The editor's own menu on a right-click in edit mode (#81): what the toolbar formats and inserts,
|
|
3
|
-
* where the author is,
|
|
3
|
+
* where the author is, after the clipboard's three verbs. It is a `role="menu"` reached by keyboard,
|
|
4
4
|
* kept inside the window, and closed by Escape, a click outside it, a scroll, or an entry run.
|
|
5
5
|
*/
|
|
6
6
|
import { type ReactNode } from 'react';
|
package/dist/edit/ContextMenu.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* The editor's own menu on a right-click in edit mode (#81): what the toolbar formats and inserts,
|
|
4
|
-
* where the author is,
|
|
4
|
+
* where the author is, after the clipboard's three verbs. It is a `role="menu"` reached by keyboard,
|
|
5
5
|
* kept inside the window, and closed by Escape, a click outside it, a scroll, or an entry run.
|
|
6
6
|
*/
|
|
7
7
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
package/dist/edit/NoteEditor.js
CHANGED
|
@@ -115,6 +115,8 @@ function insertBlockCommand(name) {
|
|
|
115
115
|
const NOT_ATTACHING = { state: 'idle', message: null, fraction: null, queued: 0 };
|
|
116
116
|
/** How long the attach button says a file was not attached, when the host did not say why, in milliseconds. */
|
|
117
117
|
const ATTACH_FAILED_MS = 3000;
|
|
118
|
+
/** The keys that write besides a character's own: a note held for the author's choice asks again on them (#101). */
|
|
119
|
+
const WRITING_KEYS = new Set(['Enter', 'Backspace', 'Delete', 'Tab']);
|
|
118
120
|
/** A host's fraction, kept between 0 and 1, or none when it gave no number. */
|
|
119
121
|
function fractionOf(report) {
|
|
120
122
|
const fraction = report?.fraction;
|
|
@@ -144,25 +146,21 @@ function Toolbar({ view, host, end, opened, onAttach, attaching, attachButton, }
|
|
|
144
146
|
* each state, in a slot of one size, so the toolbar never moves as the author types; its word is
|
|
145
147
|
* its title. A failed save's icon is the button that tries again (#66). When the host said why —
|
|
146
148
|
* an alert on a save that succeeded, or the reason for one it refused — the icon is the button that
|
|
147
|
-
* opens what it said, with the host's title as its tooltip, and the author's choices (#86)
|
|
148
|
-
*
|
|
149
|
+
* opens what it said, with the host's title as its tooltip, and the author's choices (#86); a notice
|
|
150
|
+
* with choices opens by itself as it arrives, and a refusal with choices holds the note until the
|
|
151
|
+
* author chooses (#101). Whoever hears the page is told the whole of it, why the note stays in edit
|
|
152
|
+
* included.
|
|
149
153
|
*/
|
|
150
|
-
function SaveStatus({ saves, blocked, host, onDecided }) {
|
|
154
|
+
function SaveStatus({ saves, blocked, deciding, open, setOpen, host, onDecided, }) {
|
|
151
155
|
const { state, notice } = saves;
|
|
152
156
|
const words = host.strings.editor;
|
|
153
|
-
const [open, setOpen] = useState(false);
|
|
154
157
|
const button = useRef(null);
|
|
155
|
-
// A notice the save no longer carries has nothing left to show.
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
if (!notice)
|
|
158
|
-
setOpen(false);
|
|
159
|
-
}, [notice]);
|
|
160
158
|
const base = state === 'saving' ? words.saving : state === 'saved' ? host.strings.saved : state === 'failed' ? host.strings.notSaved : '';
|
|
161
|
-
const shown = notice ? notice.title : base;
|
|
162
|
-
const said = state === 'failed' && blocked ? (notice ? `${words.stayInEdit}. ${
|
|
159
|
+
const shown = notice ? (deciding ? `${notice.title}. ${words.chooseToEdit}` : notice.title) : base;
|
|
160
|
+
const said = state === 'failed' && blocked ? (notice ? `${words.stayInEdit}. ${shown}` : words.stayInEdit) : shown;
|
|
163
161
|
const icon = state === 'saving' ? ICONS.saving : state === 'saved' ? ICONS.saved : state === 'alert' ? ICONS.alert : state === 'failed' ? ICONS.notSaved : null;
|
|
164
162
|
const retryLabel = `${said}. ${words.retry}`;
|
|
165
|
-
return (_jsxs("div", { className: "edit-save", "data-save": state, title: state === 'failed' || notice ? undefined : said || undefined, children: [notice ? (_jsx("button", { ref: button, type: "button", className: "edit-save-icon edit-save-notice", title:
|
|
163
|
+
return (_jsxs("div", { className: "edit-save", "data-save": state, title: state === 'failed' || notice ? undefined : said || undefined, children: [notice ? (_jsx("button", { ref: button, type: "button", className: "edit-save-icon edit-save-notice", title: shown, "aria-label": shown, "aria-haspopup": "dialog", onClick: () => setOpen(true), children: icon })) : state === 'failed' ? (_jsx("button", { type: "button", className: "edit-save-icon edit-save-retry", title: retryLabel, "aria-label": retryLabel, onClick: saves.retry, children: icon })) : (_jsx("span", { className: "edit-save-icon", children: icon })), _jsx("span", { className: "visually-hidden", role: "status", "aria-live": "polite", children: said }), open && notice && (_jsx(SaveNoticeDialog, { notice: notice, onClose: () => setOpen(false), onClosed: (chosen) => (chosen ? onDecided() : button.current?.focus()), onAction: (action) => {
|
|
166
164
|
setOpen(false);
|
|
167
165
|
saves.run(action);
|
|
168
166
|
} }))] }));
|
|
@@ -181,8 +179,18 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
181
179
|
const environment = useRef(new Compartment());
|
|
182
180
|
const access = useRef(new Compartment());
|
|
183
181
|
const saves = useEditSaves(onChange);
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
/**
|
|
183
|
+
* A refusal that gives the author choices — another person wrote the note, say — is decided
|
|
184
|
+
* before the note is written in again: every save after it would be refused too (#101).
|
|
185
|
+
*/
|
|
186
|
+
const deciding = saves.state === 'failed' && (saves.notice?.actions.length ?? 0) > 0;
|
|
187
|
+
const [noticeOpen, setNoticeOpen] = useState(false);
|
|
188
|
+
// A notice with choices opens by itself as it arrives; one the save no longer carries closes.
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
setNoticeOpen(!!saves.notice && saves.notice.actions.length > 0);
|
|
191
|
+
}, [saves.notice]);
|
|
192
|
+
const latest = useRef({ hand: saves.hand, host: resolved, onLeave, deciding, ask: () => setNoticeOpen(true) });
|
|
193
|
+
latest.current = { hand: saves.hand, host: resolved, onLeave, deciding, ask: () => setNoticeOpen(true) };
|
|
186
194
|
/**
|
|
187
195
|
* The texts handed to the host that it has not given back yet. A host that saves before it
|
|
188
196
|
* passes the note again gives back a text the editor has since moved past; that echo is not a
|
|
@@ -253,7 +261,7 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
253
261
|
/** Where the editing menu is open, in the window's coordinates (#81). */
|
|
254
262
|
const [menu, setMenu] = useState(null);
|
|
255
263
|
const openMenu = useRef((x, y) => setMenu({ x, y }));
|
|
256
|
-
/** The menu's groups, from the same commands as the toolbar: Format with a selection, Insert
|
|
264
|
+
/** The menu's groups, from the same commands as the toolbar: Edit first (#110), Format with a selection, Insert. */
|
|
257
265
|
const menuGroups = (target) => {
|
|
258
266
|
const words = resolved.strings.editor;
|
|
259
267
|
const item = (name, action, shortcut) => ({
|
|
@@ -300,7 +308,8 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
300
308
|
{ name: 'attach', label: words.attach, icon: ICONS.attach, run: () => setAttachOpen(true) },
|
|
301
309
|
],
|
|
302
310
|
});
|
|
303
|
-
|
|
311
|
+
// The clipboard's verbs open the menu, as they open every other one a person knows (#110).
|
|
312
|
+
groups.unshift({
|
|
304
313
|
label: words.menuEdit,
|
|
305
314
|
items: [
|
|
306
315
|
{
|
|
@@ -336,7 +345,9 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
336
345
|
// The note as edit mode opened it, as the editor holds its text: lines joined by `\n`.
|
|
337
346
|
const opened = useRef(given.replace(/\r\n?/g, '\n'));
|
|
338
347
|
const lineBreak = lineBreakOf(given);
|
|
339
|
-
const
|
|
348
|
+
const locked = !editable || lineBreak === null;
|
|
349
|
+
// Held while the author decides: the caret still moves, and a key that would write asks again.
|
|
350
|
+
const readOnly = locked || deciding;
|
|
340
351
|
/**
|
|
341
352
|
* The editor splits its text on any line break, so a paste or a block written with `\n` is lines
|
|
342
353
|
* like any other, and writes the note back with the break it was written with: what a host
|
|
@@ -353,7 +364,7 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
353
364
|
state: EditorState.create({
|
|
354
365
|
doc: given,
|
|
355
366
|
extensions: [
|
|
356
|
-
access.current.of([EditorState.readOnly.of(readOnly), EditorView.editable.of(!
|
|
367
|
+
access.current.of([EditorState.readOnly.of(readOnly), EditorView.editable.of(!locked)]),
|
|
357
368
|
environment.current.of(editorEnvironment.of({ host: resolved })),
|
|
358
369
|
history(),
|
|
359
370
|
markdown(),
|
|
@@ -373,6 +384,16 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
373
384
|
return true;
|
|
374
385
|
}),
|
|
375
386
|
EditorView.domEventHandlers({
|
|
387
|
+
// A key that would write, while the note waits for the author's choice, shows the choice again (#101).
|
|
388
|
+
keydown: (event) => {
|
|
389
|
+
if (!latest.current.deciding || event.ctrlKey || event.metaKey || event.altKey)
|
|
390
|
+
return false;
|
|
391
|
+
if (event.key.length !== 1 && !WRITING_KEYS.has(event.key))
|
|
392
|
+
return false;
|
|
393
|
+
event.preventDefault();
|
|
394
|
+
latest.current.ask();
|
|
395
|
+
return true;
|
|
396
|
+
},
|
|
376
397
|
// The editing menu where the pointer is; Shift keeps the browser's, and so do the
|
|
377
398
|
// drawn blocks' own fields, which are the browser's to edit (#81).
|
|
378
399
|
contextmenu: (event, target) => {
|
|
@@ -436,8 +457,8 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
436
457
|
view?.dispatch({ effects: environment.current.reconfigure(editorEnvironment.of({ host: resolved })) });
|
|
437
458
|
}, [view, resolved]);
|
|
438
459
|
useEffect(() => {
|
|
439
|
-
view?.dispatch({ effects: access.current.reconfigure([EditorState.readOnly.of(readOnly), EditorView.editable.of(!
|
|
440
|
-
}, [view, readOnly]);
|
|
460
|
+
view?.dispatch({ effects: access.current.reconfigure([EditorState.readOnly.of(readOnly), EditorView.editable.of(!locked)]) });
|
|
461
|
+
}, [view, readOnly, locked]);
|
|
441
462
|
// A note the host changed — a tick saved, another device — is taken in as the change it is.
|
|
442
463
|
useEffect(() => {
|
|
443
464
|
if (!view)
|
|
@@ -513,7 +534,7 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
513
534
|
else
|
|
514
535
|
setRefusal(null);
|
|
515
536
|
setAttachOpen(true);
|
|
516
|
-
}, attaching: attaching, attachButton: attachButton, end: _jsxs("div", { className: "edit-corner", "aria-busy": leaving && saves.state === 'saving' ? true : undefined, "data-blocked": blocked ? '' : undefined, children: [_jsx(SaveStatus, { saves: saves, blocked: blocked, host: resolved, onDecided: () => view?.focus() }), corner ?? _jsx(NoteActions, { display: buttons, text: () => (view ? output(view.state) : given) })] }, blocked ? `blocked-${refusals}` : 'corner') }) }), menu && view && (_jsx(ContextMenu, { x: menu.x, y: menu.y, label: resolved.strings.editor.menu, groups: menuGroups(view), onClose: (back) => {
|
|
537
|
+
}, attaching: attaching, attachButton: attachButton, end: _jsxs("div", { className: "edit-corner", "aria-busy": leaving && saves.state === 'saving' ? true : undefined, "data-blocked": blocked ? '' : undefined, children: [_jsx(SaveStatus, { saves: saves, blocked: blocked, deciding: deciding, open: noticeOpen, setOpen: setNoticeOpen, host: resolved, onDecided: () => view?.focus() }), corner ?? _jsx(NoteActions, { display: buttons, text: () => (view ? output(view.state) : given) })] }, blocked ? `blocked-${refusals}` : 'corner') }) }), menu && view && (_jsx(ContextMenu, { x: menu.x, y: menu.y, label: resolved.strings.editor.menu, groups: menuGroups(view), onClose: (back) => {
|
|
517
538
|
setMenu(null);
|
|
518
539
|
if (back)
|
|
519
540
|
view.focus();
|
package/dist/edit/blocks.d.ts
CHANGED
|
@@ -29,12 +29,28 @@ export declare function CalloutBlock({ view, text, marker }: {
|
|
|
29
29
|
fold: '+' | '-' | null;
|
|
30
30
|
};
|
|
31
31
|
}): import("react").JSX.Element;
|
|
32
|
+
/** The narrowest an image is sized to, by a field or the handle: something a reader still sees (#105). */
|
|
33
|
+
export declare const MIN_IMAGE_SIZE = 16;
|
|
32
34
|
/**
|
|
33
|
-
* An image drawn in edit mode with
|
|
34
|
-
* its height
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* that
|
|
35
|
+
* An image drawn in edit mode with what its notation writes set in place (#98, #106): its width and
|
|
36
|
+
* its height with a lock between them — locked, the height follows and only the width is written,
|
|
37
|
+
* `|300`; open, both are, `|300x180` — the width's unit, pixels or a share of the page (`|50%`,
|
|
38
|
+
* #108), its description for a Markdown image, a button back to its own size, and a handle at its
|
|
39
|
+
* corner that drags it, keeping the proportion or, with the lock open or Shift held, not. Each
|
|
40
|
+
* writes the dimensions or the description of its notation, and no other byte.
|
|
41
|
+
*
|
|
42
|
+
* A field selects what it holds when it takes the focus, so typing replaces it, and writes on Enter
|
|
43
|
+
* or when it is left; Escape leaves the note as it is. A value outside the range a reader can see
|
|
44
|
+
* — from 16 pixels to the column or the image's own width, the wider — is refused where it was
|
|
45
|
+
* typed, never brought into the range and written (#105).
|
|
46
|
+
*
|
|
47
|
+
* The controls stand under the picture, never over it, and nothing that starts on them or in the
|
|
48
|
+
* moment after reaches the click that shows the image's source (#107): they are all marked as
|
|
49
|
+
* controls, gaps included, and the frame is marked busy while one of them has the focus, while the
|
|
50
|
+
* handle is dragged and for a moment after a pointer or a touch on them.
|
|
51
|
+
*
|
|
52
|
+
* The controls appear once an image is drawn inside it, so an embed of a file that is not an image,
|
|
53
|
+
* or a host's own drawing without one, offers none.
|
|
38
54
|
*/
|
|
39
55
|
export declare function ImageBlock({ view, source, children }: {
|
|
40
56
|
view: EditorView;
|